/*
 * Copyright (c) 2007-2008, Thomas Krause (tho.krause@gmail.com)
 * 
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright notice,
 *       this list of conditions and the following disclaimer in the documentation
 *       and/or other materials provided with the distribution.
 *     * Neither the name of gidoo nor the names of its contributors may be used to
 *       endorse or promote products derived from this software without specific
 *       prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.gidoo.cms.dbconnection;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.NoResultException;
import javax.persistence.Persistence;
import org.gidoo.cms.helper.Tools;
import org.gidoo.cms.data.CMSSection;
import org.gidoo.cms.data.Folder;
import org.gidoo.cms.data.Page;
import org.gidoo.cms.data.RegisteredUser;
import org.gidoo.cms.data.Resource;

/**
 * This class handles the connection to the database and all data retrieval
 * and storage.
 * @author <a href="mailto:tho.krause@gmail.com">Thomas Krause</a>
 */
public class Persistance 
{
 
  private EntityManagerFactory emf;
  
  private EntityManager em;
  
  public Persistance()
  {
    emf = Persistence.createEntityManagerFactory("gidoocmsPU");
    em = emf.createEntityManager();
  }
  
  /**
   * Returns a list of all registered users of the CMS.
   */
  public List<RegisteredUser> getAllUsers() throws DatabaseConnectionException
  {
    List<RegisteredUser> users = null;
    try
    {
      users = (List<RegisteredUser>) em.createQuery("select r from " +
              "RegisteredUser r order by r.loginName").getResultList();
    }
    catch(Exception ex)
    {
      Logger.getLogger(Persistance.class.getName()).log(Level.SEVERE, null, ex);
      throw new DatabaseConnectionException(ex.getMessage());
    }
    return users;
  }
  
  /**
   * Get the user information by the login. 
   * 
   * @param login The user login name.
   * @return The user information or null if not existant.
   */
  public RegisteredUser getUserByLogin(String login) throws DatabaseConnectionException
  {
    if(login == null)
      return null;
        
    RegisteredUser user = null;
    
    try
    {
      user = (RegisteredUser)
        em.createQuery("select r from RegisteredUser r where r.loginName='" + login + "'").getSingleResult();
    }
    catch(NoResultException nrEx)
    {
      user = null;
    }
    catch(Exception ex)
    {
      Logger.getLogger(Persistance.class.getName()).log(Level.SEVERE, null, ex);
      throw new DatabaseConnectionException(ex.getMessage());
    }
    
    return user;
  }
  
  /**
   * Returns the section or null if not found.
   * @param url The exact URL of the section
   */
  public CMSSection getSectionByExactUrl(String url) throws DatabaseConnectionException
  {
    if(url == null)
      return null;
    
    if("/".equals(url))
    {
      url = "";
    }
    
    CMSSection cmssection = null;
    try
    {
      cmssection = (CMSSection)
        em.createQuery("select s from CMSSection s where s.url='" + url + "'").getSingleResult();
    }
    catch(NoResultException nrEx)
    {
      cmssection = null;
    }
    catch(Exception ex)
    {
      Logger.getLogger(Persistance.class.getName()).log(Level.SEVERE, null, ex);
      throw new DatabaseConnectionException(ex.getMessage());
    }
    
    return cmssection;
    
  }
  
  /** Get a list of all sections of the CMS */
  public List<CMSSection> getAllSections() throws DatabaseConnectionException
  {
    List<CMSSection> sections = null;
    try
    {
      sections = (List<CMSSection>) em.createQuery("select s from CMSSection s " +
              "order by s.position").getResultList();
    }
    catch(Exception ex)
    {
      Logger.getLogger(Persistance.class.getName()).log(Level.SEVERE, null, ex);
      throw new DatabaseConnectionException(ex.getMessage());
    }
    return sections;
  }
  
  /**
   * Returns the section to which a path of the CMS belongs to.
   * @param path The path of the CMS.
   * @return The section or null if no section was found.
   */
  public CMSSection getSectionByPath(String path) throws DatabaseConnectionException
  {
    if(path == null)
      return null;
    
    List<CMSSection> sections = getAllSections();

    // some cleanup
    path = Tools.cleanupFolderPath(path);
    
    int bestMatchLength = -1;
    CMSSection bestMatchSection = null;
    
    for(CMSSection s : sections)
    {
      String u = s.getUrl();
      
      Pattern p = Pattern.compile(u);
      Matcher m = p.matcher(path);
      
      // does the string begin with the pattern and is it longer than the last match?
      if(m.find() && m.start() == 0 && u.length() > bestMatchLength)
      {
        // true if either the string matches exactly or if the next character is a "/"
        if(u.length() == 0 || path.length() == m.end() || path.charAt(m.end()) == '/')
        {
          bestMatchLength = u.length();
          bestMatchSection = s;
        }
      }
    }
    
    return bestMatchSection;
  }
  
  /**
   * Retreive the most recent and published page by the path.
   * 
   * @param path The path of the page (without section part).
   * @param section The section to which this path belongs to.
   * @return The page or null if not found.
   */
  public Page getPageByPath(String path, CMSSection section) throws DatabaseConnectionException
  {
    Folder folder = getFolderByPath(path, section);
    if(folder != null)
    {
      if(Tools.isPublished(folder))
      {        
        return folder.getPublishedPage();
      }
      else
      {
        return null;
      }
    }
    else
    {    
      return null;
    }
  }
  
  /**
   * Retreive the folder by it's path.
   * 
   * @param path The path of the page (with section part).  
   * @return The folder or null if not found.
   */
  public Folder getFolderByPath(String path) throws DatabaseConnectionException
  {
    if(path == null)
      return null;
    
    CMSSection section = getSectionByPath(path);
    if(section == null)
      return null;
    
    String urlWithoutSection = path.substring(section.getUrl().length());
    
    return getFolderByPath(urlWithoutSection, section);
  }
  
  /**
   * Retreive a published resource by the path.
   * 
   * @param path The path of the Resource (with section part)
   * @return The resource or null if not found or not published.
   * @throws org.gidoo.cms.dbconnection.DatabaseConnectionException
   */
  public Resource getResourceByPath(String path) throws DatabaseConnectionException
  {
    if(path == null)
      return null;
    
    CMSSection section = getSectionByPath(path);
    if(section == null)
      return null;
    
    String urlWithoutSection = path.substring(section.getUrl().length());
    
    urlWithoutSection = Tools.cleanupFolderPath(urlWithoutSection);
    
    int index = urlWithoutSection.lastIndexOf('/');
    
    String folderPath = "";
    String resName ="";
    
    if(index < 0)
    {      
      // no slash in path
      folderPath = "";
      resName = urlWithoutSection;
    }
    else
    {
      // take last part as resource name
      folderPath = urlWithoutSection.substring(0, index);
      resName = urlWithoutSection.substring(index+1);
    }
    
    Folder f = getFolderByPath(folderPath, section);
    if(f == null)
      return null;
    
    List<Resource> resources = f.getResources();
    if(resources != null)
    {
      for(Resource res : resources)
      {
        if(res.getName().equals(resName))
        {
          if(res.getIsPublished())
          {
            return res;
          }
          else
          {
            return null;
          }
        }
      }
    }
    
    return null;
  }
  
  /**
   * Returns the Folder which is matching to the given ID.
   * @param id The id of the folder.
   * @return The folder or null if not found;
   */
  public Folder getFolderById(long id) throws DatabaseConnectionException
  {
    Folder result = null;
    try
    {
      String query = "select f from Folder f where f.id = " + id;
      result = (Folder) em.createQuery(query).getSingleResult();
    }
    catch(NoResultException ex)
    {
      result = null;
    }
    catch(Exception ex)
    {
      Logger.getLogger(Persistance.class.getName()).log(Level.SEVERE, null, ex);
      throw(new DatabaseConnectionException(ex.getMessage()));
    }
    
    return result;
  }
  
  /**
   * Retreive a folder by it's path.
   * 
   * @param path The path (without the section-part).
   * @param section The section.
   * @return The folder belonging to this path or null if not.
   */
  public Folder getFolderByPath(String path, CMSSection section) throws DatabaseConnectionException
  {
    if(path == null || section == null)
      return null;
    
    path = Tools.cleanupFolderPath(path);
    
    Folder folder = null;
    Folder result = null;
    
    String[] splittedPath = path.split("/");
    int level = 0;
        
    try
    {
      
      String query = "select s.rootFolder from CMSSection s where s.id=" +
        section.getId();
      folder = (Folder) 
        em.createQuery(query).getSingleResult();
      
      if("".equals(path))
      {
        return folder;
      }
      
      
      boolean wasFound = true;
      
      while(wasFound && level < splittedPath.length)
      {
        List<Folder> children = getChildrenOfFolder(folder);
        wasFound = false;
        for(Folder f : children)
        {
          if(f.getName().equals(splittedPath[level]))
          {
            wasFound = true;
            level++;
            folder = f;
            break;
          }
        }
      }
      
      
      if(wasFound && folder != null)
      {
        result = folder;

      }
      
    }
    catch(NoResultException nrEx)
    {
      result = null;
    }
    catch(Exception e)
    {
      Logger.getLogger(Persistance.class.getName()).log(Level.SEVERE, null, e);
      throw(new DatabaseConnectionException(e.getMessage()));
    }
    
    
    return result;
  }
  
  /**
   * Get all children of a folder.
   * @param folder The folder you want to now the children for.
   * @return The children. Should never be null, even if there are no children.
   */
  public List<Folder> getChildrenOfFolder(Folder folder) throws DatabaseConnectionException
  {
    List<Folder> result = new ArrayList<Folder>();
    
    if(folder == null)
      return result;
    
    try
    {
      String childrenQuery = "select f from Folder f where f.parent.id=" + 
        folder.getId() + " order by f.position";
        List<Folder> children = em.createQuery(childrenQuery).getResultList();
        
      if(children != null)
      {
        result = children;
      }
    }
    catch(Exception ex)
    {
      Logger.getLogger(Persistance.class.getName()).log(Level.SEVERE, null, ex);
      throw(new DatabaseConnectionException(ex.getMessage()));
    }
    
    return result;
  }
    
  /**
   * Will store a new object in the database. If you want to update existing
   * objects use #merge.
   * @param object The new object.
   * @throws org.gidoo.cms.dbconnection.DatabaseConnectionException
   */
  
  public void persist(Object object) throws DatabaseConnectionException
  {
    try
    {
      em.getTransaction().begin();
      em.persist(object);
      em.getTransaction().commit();
    } 
    catch (Exception ex)
    {
      Logger.getLogger(Persistance.class.getName()).log(Level.SEVERE, null, ex);
      if(em.getTransaction().isActive())
      {
        em.getTransaction().rollback();
      }
      throw new DatabaseConnectionException(ex.getMessage());
    }
  }
  
  /** 
   * Will store a set of new objects in the database.
   * This is done in one transaction so *all* data will be lost if an
   * error occurs.
   * @param objects An array containing the new objects.
   */
  public void persist(Object[] objects) throws DatabaseConnectionException
  {
    try
    {
      em.getTransaction().begin();
      
      for(Object o : objects)
      {
        em.persist(o);
      }
      
      em.getTransaction().commit();
      
    } catch (Exception ex)
    {
      Logger.getLogger(Persistance.class.getName()).log(Level.SEVERE, null, ex);
      if(em.getTransaction().isActive())
      {
        em.getTransaction().rollback();
      }
      throw new DatabaseConnectionException(ex.getMessage());
    } 
  }
  
  /**
   * Write an updated version of an existing object to the database.
   * 
   * @return The new object which was merged to the database. Use this
   * reference when using the changed object.
   */
  public Object merge(Object o) throws DatabaseConnectionException
  {
    try
    {
      em.getTransaction().begin();
      Object newObject = em.merge(o);   
      
      em.getTransaction().commit();
      
      return newObject;
    }
    catch(Exception ex)
    {            
      Logger.getLogger(Persistance.class.getName()).log(Level.SEVERE, null, ex);
      if(em.getTransaction().isActive())
      {
        em.getTransaction().rollback();
      }
      throw new DatabaseConnectionException(ex.getMessage());
    }
  }

  /**
   * Write a set of updated versions of existing objects to the database.
   * Will rollback *all* changes if merge fails on one of the objects.
   * 
   * @return The new objects which were merged to the database. Use this
   * reference when using the changed objects. 
   */
  public Object[] merge(Object[] objects) throws DatabaseConnectionException
  {
    try
    {
      Object[] newObjects = new Object[objects.length];
      em.getTransaction().begin();
      
      for(int i=0; i < objects.length; i++)
      {
        newObjects[i] = em.merge(objects[i]);   
      }
      
      em.getTransaction().commit();
      
      return newObjects;
    }
    catch(Exception ex)
    {            
      Logger.getLogger(Persistance.class.getName()).log(Level.SEVERE, null, ex);
      if(em.getTransaction().isActive())
      {
        em.getTransaction().rollback();
      }
      throw new DatabaseConnectionException(ex.getMessage());
    }
  }
  
  /**
   * Removes an object from the database. Be careful with cascading objects since
   * you might delete more than you wanted to.
   * @param o The object to remove.
   * @throws org.gidoo.cms.dbconnection.DatabaseConnectionException
   */
  public void remove(Object o) throws DatabaseConnectionException
  {
    try
    {
      if(o == null)
        return;
      
      em.getTransaction().begin();
      em.remove(o);
      em.getTransaction().commit();
      
    }
    catch(Exception ex)
    {
      Logger.getLogger(Persistance.class.getName()).log(Level.SEVERE, null, ex);
      if(em.getTransaction().isActive())
      {
        em.getTransaction().rollback();
      }
      throw new DatabaseConnectionException(ex.getMessage());
    }
  }

  /**
   * Merge a list of folders which belong to the same parent. This
   * function will set the position attribute of the folders in that way,
   * that they are consistant and after each other (as in the list). 
   * 
   * @param siblings Ordered list of <b>all</b> of the siblings of a folder.
   */
  public List<Folder> mergeListOfFolders(List<Folder> siblings)
    throws DatabaseConnectionException
  {
    List<Folder> result = new LinkedList<Folder>();
    if(siblings != null && siblings.size() > 0)
    {
      try
      {
        // first set all to some non-existant values        
        em.getTransaction().begin();
        
        Iterator<Folder> it = siblings.iterator();
        int i=0;
        while(it.hasNext())
        {
          Folder currentFolder = it.next();
          currentFolder.setPosition(-(i+1));
          em.merge(currentFolder);
          i++;
        }
        
        em.flush();
        
        // now the real values
        it = siblings.iterator();
        i=0;
        while(it.hasNext())
        {
          Folder currentFolder = it.next();
          currentFolder.setPosition(i);
          result.add((Folder) em.merge(currentFolder));
          i++;
        }
        em.getTransaction().commit();
      }
      catch(Exception ex)
      {
        Logger.getLogger(Persistance.class.getName()).log(Level.SEVERE, null, ex);
        throw(new DatabaseConnectionException(ex.getMessage()));
      }
    }
    return result;
  }
  
  /**
   * This function will set the position attribute of the sections in that way,
   * that they are consistant and after each other (as in the list). 
   * 
   * @param siblings Ordered list of <b>all</b> of the siblings of the sections.
   */
  public List<CMSSection> mergeListOfSections(List<CMSSection> siblings)
    throws DatabaseConnectionException
  {
    List<CMSSection> result = new LinkedList<CMSSection>();
    if(siblings != null && siblings.size() > 0)
    {
      try
      {
        // first set all to some non-existant values        
        em.getTransaction().begin();
        
        Iterator<CMSSection> it = siblings.iterator();
        int i=0;
        while(it.hasNext())
        {
          CMSSection currentSection = it.next();
          currentSection.setPosition(-(i+1));
          em.merge(currentSection);
          i++;
        }
        
        em.flush();
        
        // now the real values
        it = siblings.iterator();
        i=0;
        while(it.hasNext())
        {
          CMSSection currentSection = it.next();
          currentSection.setPosition(i);
          result.add((CMSSection) em.merge(currentSection));
          i++;
        }
        em.getTransaction().commit();
      }
      catch(Exception ex)
      {
        Logger.getLogger(Persistance.class.getName()).log(Level.SEVERE, null, ex);
        throw(new DatabaseConnectionException(ex.getMessage()));
      }
    }
    return result;
  }
  
  /**
   * Get the EntityManager used by this class. This can be useful
   * when you want to do JPA-stuff that is not mapped by this class.
   */
  public EntityManager getEm()
  {
    return em;
  }

  

}
